package dps;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.swing.JFrame;
import javax.swing.Timer;

import networking.DatagramClient;
import networking.DatagramServer;

import org.jnetpcap.packet.JPacket;
import org.jnetpcap.packet.format.FormatUtils;
import org.jnetpcap.protocol.network.Ip4;

import dps.DpsMessageProto.DpsMessage;
import dps.DpsMessageProto.DpsMessage.MessageType;
import dps.DpsMessageProto.DpsMessage.VoteType;
import frameTools.FrameReader;

public class DpsMain {

	private static DPS_GUI frame;
	
	//List of all local events
	private static List<LocalEvent> lstLocalEventData = Collections.synchronizedList(new ArrayList<LocalEvent>());
    
	//Map of all node's (or voters) degree of trust
	private static Map<UUID, Trust> mapVoterTrust = Collections.synchronizedMap(new HashMap<UUID, Trust>());
	    
	//List of all node's degree of trust
	private static List<Vote> lstVote = Collections.synchronizedList(new ArrayList<Vote>());
    
    //Map of all polls
	private static Map<UUID, Poll> mapPoll = Collections.synchronizedMap(new HashMap<UUID, Poll>());
    
	public static String LOCAL_IP  = "192.168.1.2";
	public static final int LOCAL_PORT = 6667;
	public static String REMOTE_IP = "236.0.0.1";	// should be broadcast
	
	public static int POLL_WAIT_TIME       = 10000; // in ms
	public static int RANDOM_RESPONSE_TIME = 500; // in ms - larger than wait to show unresponsive nodes
	
	public static String VOTER_ID = "";
	
	/**************************************
	 *   
	 *       MAIN
	 * @throws UnknownHostException 
	 * @throws InterruptedException 
	 * 
	 **************************************/
    public static void main(String[] args) throws UnknownHostException, InterruptedException 
    {
    	/**
    	 * Allow clients to get local/remote ip from command line
    	 */
    	
    	int numPolls = 0;
    	String pollParam = "192.168.1.2";
    	for(int i = 0; i < args.length; ++i) {
    		if(args[i].equals("-rip")) {
    			REMOTE_IP = args[i+1];
    			System.out.println("using provided IP... " + REMOTE_IP);
    		} else if(args[i].equals("-sim")) {
    			numPolls = Integer.parseInt(args[i+1]); 
    			System.out.println("Simulating polls... " + numPolls + " of them.");
    		} else if(args[i].equals("-sim2")) {
    			numPolls = Integer.parseInt(args[i+1]);
    			pollParam = args[i+2];
    			System.out.println("Simulating polls... " + numPolls + " of them for pollParam " + pollParam);
    		} else if(args[i].equals("-rvid")) {
    			VOTER_ID = String.valueOf(UUID.randomUUID());
    			System.out.println("Using random VoterID as opposed to the one retrieved from the file");
    		} else if(args[i].equals("-?")) {
    			System.out.println("-rip <RemoteIP>");
    			System.out.println("-sim <# polls to sim>");
    			System.out.println("-rvid // allows for multiple instances properly to run locally");
    			System.out.println("-pwt // sets the poll wait time (till votes are tallied)");
    			System.out.println("-rrt // sets the random response time (till votes are cast)");
    			System.exit(0);
    		} else if(args[i].equals("-pwt")) {    			
    			POLL_WAIT_TIME = Integer.parseInt(args[i+1]);
    			System.out.println("Using poll wait time of " + POLL_WAIT_TIME + "ms");
    		} else if(args[i].equals("-rrt")) {    			
    			RANDOM_RESPONSE_TIME = Integer.parseInt(args[i+1]);
    			System.out.println("Using random response time of " + RANDOM_RESPONSE_TIME + "ms");
    		}
    	}
    	
    	System.out.println("Local IP = InetAddress.getLocalHost() says, " + InetAddress.getLocalHost());
    	System.out.println("Remote IP = " + REMOTE_IP);
    	System.out.println("Remote IP should be a multicast group address....\n\n");
    	
    	new FrameReader();
    	try {
			new DatagramServer();
		} catch (IOException e) {
			e.printStackTrace();
		}
    	
    	/**
    	 * Each voter must have a unique ID that is relatively static.  Creating a UUID and 
    	 * saving it to a file locally is likely more static than an ip or MAC address as 
    	 * they may change.  This does mean that uninstalling/changing machines or mucking
    	 * around with the file will change the identifier.  Still open to alternatives....
    	 */
    	BufferedReader br;
		try {
			if(VOTER_ID.isEmpty()) {
				br = new BufferedReader(new FileReader("voterId.txt"));
				VOTER_ID = br.readLine();
				br.close();
			}
		} catch (FileNotFoundException e) {
			VOTER_ID = UUID.randomUUID().toString();
			try {
				BufferedWriter bw = new BufferedWriter(new FileWriter(new File("voterId.txt")));
				bw.write(VOTER_ID);
				bw.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
				
    	//Invoking the GUI 
    	frame = new DPS_GUI();
	    
    	//Setting the GUI frame properties
	    frame.setTitle("DPS GUI");
	    frame.setSize(900,500);
	    frame.setLocationRelativeTo(null);
	    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	    frame.setVisible(true);
	    
	    /**
	     * test setup?!
	     */
	    if(numPolls>0)
	    	for(int i=0; i < numPolls; ++i)
				try {
					CreateAndSendPollMsg(pollParam);
				} catch (IOException e) {
					e.printStackTrace();
				}

	    LocalEvent e = new LocalEvent();
	    e.eventId = UUID.randomUUID();
	    e.strEventParam = "localhost";
	    lstLocalEventData.add(e);
    }
    
    /**
     * This method is called by the framereader and adds to the local packet
     * history.  The messages are the packets read in from the interface.
     * The packets destined for LOCAL_PORT are excluded from this set so we
     * aren't collecting polling communication
     * 
     * @param msg
     * @throws IOException
     */
    public static void addBuff(DpsMessage msg) throws IOException
    {
    	if(msg.getMessageType() == MessageType.POLL_REQUEST)
    	{
    		/**
    		 * Find the topic in our local history
    		 */
			int numItemsFound = 0;
			synchronized (lstLocalEventData) {
				Iterator<LocalEvent> i = lstLocalEventData.iterator();				
				while (i.hasNext()) {
					LocalEvent e = (LocalEvent) i.next();
					if (e.strEventParam.equals(msg.getPollParam())) {
						// do something neat!
						numItemsFound++;
					}
				}

				if (0 < numItemsFound)
					System.out.println("Poll parameter found " + numItemsFound + " times");
				else
					System.out.println("Poll parameter not found");
			}
    		
			VoteType vote = VoteType.NO_RESPONSE;
			if (0 < numItemsFound) {
	    		/**
	    		 * Since we aren't writing the classifier, we're going
	    		 * to just arbitrarily respond y/n/? to the poll requests
	    		 */
	    		int v = (int) (Math.random() * 2);	    		
	    		switch(v) {
		    		case 0:  vote = VoteType.NO; break;
		    		case 1:  vote = VoteType.YES; break;
		    		default: System.err.println("Unknown VoteType");
	    		}
			}
    		
    		DpsMessage retMsg = DpsMessage.newBuilder(msg).setMessageType(MessageType.VOTE_RESPONSE)
    				                                      .setPollVote(vote)
    				                                      .setVoterId(VOTER_ID)
    				                                      .setMessageId(UUID.randomUUID().toString())
    				                                      .setVoterAddress(InetAddress.getLocalHost().toString())
    				                                      .build();
    		
    		final DpsMessage response = retMsg;
    		int delay = (int) (Math.random() * RANDOM_RESPONSE_TIME);
    		ActionListener randomResponseTimer = new ActionListener() {
    			@Override
    			public void actionPerformed(ActionEvent arg0) {
    				DatagramClient c;
					try {
						c = new DatagramClient(REMOTE_IP, LOCAL_PORT);
						c.sendMessage(response);
					} catch (IOException e1) {
						e1.printStackTrace();
					}
    			}
    		};
    		
    		Timer t = new Timer(delay, randomResponseTimer);
    		t.setRepeats(false);
    		t.start();
		} 
    	else if (msg.getMessageType() == MessageType.VOTE_RESPONSE) 
    	{
			synchronized (mapPoll) {
				synchronized (lstVote) {
					synchronized (mapVoterTrust) {
						// Vote isn't for a poll we've initiated, move on
			    		if(mapPoll.containsKey(UUID.fromString(msg.getPollId())) == false)
			    			return;
			
						// Check the poll id before adding the message
						Vote vote = new Vote();
	
						// Protocol Buffer needs to change poll id should be
						// type uuid
						vote.pollId = UUID.fromString(msg.getPollId());
						vote.voterId = UUID.fromString(msg.getVoterId());
						vote.strPollParam = msg.getPollParam();
						vote.voterAddress = msg.getVoterAddress();
						vote.degreeOfTrust = GetDegreeOfTrust(vote.voterId);
			
				    	switch( msg.getPollVote().toString())
				    	{
					    	case "YES": vote.vote = dps.Vote.VoteType.YES; break;
					    	case "NO": vote.vote = dps.Vote.VoteType.NO; break;
					    	case "NO_RESPONSE": vote.vote = dps.Vote.VoteType.NoResponse; break;
					    	default : vote.vote = dps.Vote.VoteType.NoResponse; break;
				    	}
			
						// Adding vote to list of votes
						lstVote.add(vote);
					}
				}
			}
		}
	}
    
    //This just increments the total number of votes
    // for that voter provided in the trust map
    public static void AddVoteToTrust( Vote v )
    {
    	Trust t = ( Trust ) mapVoterTrust.get( v.voterId );
    	
    	// do we have the voterId in the Trust Map
    	if( t == null ) {    	    		
    		t = new Trust();
    		t.voterId = v.voterId;
    		t.degreeOfTrust = 0;
    		mapVoterTrust.put( t.voterId, t );
    	}
    	
    	//Increment the total number of votes
    	// casted by this voter
    	t.voterTotalNumOfVotes ++;
    }
    
    
    //This is just adding an incoming vote to the poll list
    public static void AddVoteToPoll( Vote v )
    {
		 Poll poll = (Poll) mapPoll.get(v.pollId);
		 
		 if(poll == null)
			 return;
		 
		 //Increasing the vote in the poll
		 switch( v.vote.toString() )
		 {
			 case "YES" : 
				 poll.pollVoteYes++; 
				 break;
			 case "NO" : 
				 poll.pollVoteNo++;
				 break;
			 case "NoResponse" : 
				 poll.pollVoteNoResponse++; 
				 break;
			 default : 
				 poll.pollVoteNoResponse++; 
		 }
    }
    
    
    //This method returns the trust value for the voter
    // with the voterId
    public static double GetDegreeOfTrust( UUID vId )
    {
    	 Trust v = (Trust) mapVoterTrust.get(vId);
    	 if(v == null) {
    		 return 0.0;
    	 }
    	 else
    		 return v.degreeOfTrust;
    }
    
    /******************************************
     * 
     * This method is adding the local event object to 
     * the global list of local events arraylist
     * for building the local event history
     *
     *******************************************/
    public static void addPacket(JPacket pkt) 
    {
    	Ip4 ip = new Ip4();
    	UUID uid =  UUID.randomUUID();
    	LocalEvent localEvent = new LocalEvent();
    	//Date date1 = new Date(pkt.getCaptureHeader().timestampInMillis());
    	
    	//Checks for IPv4 header
		if ( pkt.hasHeader( ip ))
		{
			localEvent.strEventParam = FormatUtils.ip(ip.source());
			
			//Adding date and time for the event
			//from the packet received
			localEvent.dtTimeStamp = new Date(pkt.getCaptureHeader().timestampInMillis());
			
			//Adding a unique id for event
			localEvent.eventId = uid;
			
			//All events are considered safe
			localEvent.eventSafe = LocalEvent.EventSafe.YES;
			
			//Adding the local event object to 
			//list of local events
			synchronized(lstLocalEventData) { 
				lstLocalEventData.add(localEvent);
			}
		}
    }

    public static void voteFinished(String pollID) {
    	// Ensure that mapPoll isn't touched while updating GUI
    	synchronized ( mapPoll ) {
    		synchronized (lstVote) {
    			synchronized (mapVoterTrust) {
		    		Poll poll = ( Poll ) mapPoll.get( UUID.fromString( pollID ));
			    	
			    	if( poll == null ) {
			    		System.err.println("Err... our own poll doesn't exist in our poll map?!");
			    		return;
			    	}
			    	
			    	//Updates dot for all voters
			    	UpdateTrustForAllVoters( poll );
			    	
			    	//Adding the DOT values to the poll 
			    	PollUpdate( poll );
			    	
			    	frame.PollHistoryGrid( mapPoll );	    	
			    	frame.PollResult( poll );
			    	frame.DOTGrid( mapVoterTrust );
			    	System.out.println("Vote finished!");
	    		}
    		}
    	}
    }
    
    // Adding the DOT 
 public static void PollUpdate( Poll p )
 {
	 Iterator<Vote> i = lstVote.iterator();				
	 while (i.hasNext()) {
		 Vote v = (Vote) i.next();
		 
		 if (v.pollId.equals(p.pollId)) {
			 // Increasing the vote in the poll
			 switch (v.vote.toString()) {
			 case "YES":
				 p.pollVoteYes = p.pollVoteYes + GetDegreeOfTrust(v.voterId);
				 break;
				case "NO":
					p.pollVoteNo = p.pollVoteNo + GetDegreeOfTrust(v.voterId);
					break;
				case "NoResponse":
					p.pollVoteNoResponse = p.pollVoteNoResponse + GetDegreeOfTrust(v.voterId);
					break;
				default:
					p.pollVoteNoResponse = p.pollVoteNoResponse + GetDegreeOfTrust(v.voterId);
				}
			}
		}
	}
    
    
    public static void UpdateTrustForAllVoters( Poll p ) 
    {   	
		Iterator<Vote> i = lstVote.iterator();
		while (i.hasNext()) {
			Vote v = (Vote) i.next();

			// Finding votes all votes for a specific poll
			if (v.pollId.equals(p.pollId)) {
				// Since we've finished the poll, need to adjust the
				// trust and poll with values that the voters that
				// have responded with
				AddVoteToTrust(v);
				AddVoteToPoll(v);
			}
		}
    	
		i = lstVote.iterator();
		while (i.hasNext()) {
			Vote v = (Vote) i.next();

			// Finding votes all votes for a specific poll
			if (v.pollId.equals(p.pollId)) {
				// Increase trust if voter's vote is in the majority
				if (v.vote.equals(p.getMajority()))
					UpdateTrust(v, true);
				else
					UpdateTrust(v, false);
			}
		}
    }
    
    //This just increments the total number of safe votes
    // and degree of trust for that voter provided in the trust map
    public static void UpdateTrust( Vote v, boolean inMajority )
    {
    	Trust t = ( Trust ) mapVoterTrust.get( v.voterId );
    	
    	// do we have the voterId in the Trust Map
    	if( t == null ) {    	    		
    		t = new Trust();
    		t.voterId = v.voterId;
    		t.degreeOfTrust = 0;
    		mapVoterTrust.put( t.voterId, t );
    	}
    	
    	//Increment the total number of safe votes
    	//and update the dot for this voter
    	if(inMajority)
    		t.voterTotalNumOfSafeVotes ++;
    	t.degreeOfTrust = (double)t.voterTotalNumOfSafeVotes/(double)t.voterTotalNumOfVotes;
    	
    	System.out.println("DoT=" + t.degreeOfTrust + " " + (double)t.voterTotalNumOfSafeVotes + "/" + (double)t.voterTotalNumOfVotes);
    	
    	if(t.degreeOfTrust < .2)
    		System.out.println("WARNING: " + v.voterId + " has a very low degree of trust");
//    		JOptionPane.showMessageDialog(frame, "WARNING: " + v.voterId + " has a very low degree of trust");
    }
    
    
	public static void CreateAndSendPollMsg(String strPollParam) throws IOException {
		
		frame.lblTotalNoResponseVotes.setText("");
		frame.lblTotalNoVotes.setText("");
		frame.lblTotalYesVotes.setText("");
		frame.lblFinalPollResult.setText("");
		
		// Create datagram client with destination (BROADCAST?) address and port
		DatagramClient c = new DatagramClient(REMOTE_IP, LOCAL_PORT);
		
		//Creating poll object 
		Poll poll = new Poll();
		poll.pollId = UUID.randomUUID();
		poll.strPollParam = strPollParam;
		
		synchronized (mapPoll) {
			mapPoll.put( poll.pollId, poll );
				
		
		DpsMessage msg = DpsMessage.newBuilder().setMessageId( UUID.randomUUID().toString() )
				                                .setMessageType(MessageType.POLL_REQUEST)
				                                .setPollId( poll.pollId.toString() )
				                                .setPollParam( strPollParam )
				                                .setPollVote( VoteType.NO_RESPONSE )
				                                .setPollerAddress( InetAddress.getLocalHost().toString() )
				                                .setVoterAddress( REMOTE_IP ).build();

		c.sendMessage( msg );
		
		final String pollID = msg.getPollId();
		int delay = POLL_WAIT_TIME; // in ms
		ActionListener pollTimer = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent arg0) {
				voteFinished(pollID);
			}
		};
		
		Timer t = new Timer(delay, pollTimer);
		t.setRepeats(false);
		t.start();
		}
	}
}
